using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input.Manipulations;
using System.Windows.Interactivity;
using System.Windows.Media;

namespace Wintellect.Touch
{
    public abstract class TouchScrollBehaviorBase<T> : Behavior<T> where T : DependencyObject
    {
        #region Fields
        private Orientation? _scrollDirection;
        private Manipulate2DBehavior _manipulationBehavior;
        private Boolean _originalHitTestVisibility;
        private Boolean? _usesVirtualStackPanel;

        private ScrollContentPresenter _innerPresenter;

        /// <summary>
        /// Lazy fetch of the inner presenter.
        /// </summary>
        /// <value>The inner presenter.</value>
        private ScrollContentPresenter InnerPresenter
        {
            get { return _innerPresenter ?? (_innerPresenter = AssociatedObject.GetChild<ScrollContentPresenter>()); }
        }

        #endregion

        #region Behavior Overrides

        /// <summary>
        /// Called after the behavior is attached to an AssociatedObject.
        /// </summary>
        /// <remarks>Override this to hook up functionality to the AssociatedObject.</remarks>
        protected override void OnAttached()
        {
            base.OnAttached();

            // Find an associated manipulation behavior if one exists.  If not, create one and attach it.
            var behaviors = Interaction.GetBehaviors(AssociatedObject);
            _manipulationBehavior = behaviors.FirstOrDefault(x => x is Manipulate2DBehavior) as Manipulate2DBehavior;
            if (_manipulationBehavior == null)
            {
                _manipulationBehavior = new Manipulate2DBehavior { IsInertial = true, InertiaDeceleration = (15.0 * 96.0f) / (1000.0f * 1000.0f) };

                // Defer the attach operation so it doesn't happen within this Attach, or an exception is raised.
                AssociatedObject.Dispatcher.BeginInvoke(() => behaviors.Add(_manipulationBehavior));
            }

            // Subscribe to the manipulation events
            _manipulationBehavior.ManipulationStarted += OnManipulationStarted;
            _manipulationBehavior.ManipulationDelta += OnManipulationDelta;
            _manipulationBehavior.ManipulationCompleted += OnManipulationCompleted;
        }

        /// <summary>
        /// Called when the behavior is being detached from its AssociatedObject, but before it has actually occurred.
        /// </summary>
        /// <remarks>Override this to unhook functionality from the AssociatedObject.</remarks>
        protected override void OnDetaching()
        {
            base.OnDetaching();

            // Unsubscribe from the manipulation events
            _manipulationBehavior.ManipulationStarted -= OnManipulationStarted;
            _manipulationBehavior.ManipulationDelta -= OnManipulationDelta;
            _manipulationBehavior.ManipulationCompleted -= OnManipulationCompleted;

            // Release the behavior
            _manipulationBehavior = null;
        }

        #endregion

        #region Manipulation Event Handlers

        /// <summary>
        /// Called when a manipulation is started on the <see cref="Behavior.AssociatedObject"/>.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.Input.Manipulations.Manipulation2DStartedEventArgs"/> instance containing the event data.</param>
        private void OnManipulationStarted(Object sender, Manipulation2DStartedEventArgs e)
        {
            // Make the inner presenter invisible to hit tests to prevent selection while trying to scroll
            if (InnerPresenter != null)
            {
                _originalHitTestVisibility = InnerPresenter.IsHitTestVisible;
                InnerPresenter.IsHitTestVisible = false;
            }
        }

        /// <summary>
        /// Called when a manipulation delta occurs on the <see cref="Behavior.AssociatedObject"/>.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.Input.Manipulations.Manipulation2DDeltaEventArgs"/> instance containing the event data.</param>
        private void OnManipulationDelta(Object sender, Manipulation2DDeltaEventArgs e)
        {
            // Try to pick a scroll direction based on its first commitment to being horizontal or vertical
            if (!_scrollDirection.HasValue)
            {
                // TODO: this is pretty arbitrary   
                _scrollDirection = Math.Abs(e.Velocities.LinearVelocityX) > Math.Abs(e.Velocities.LinearVelocityY)
                                       ? Orientation.Horizontal
                                       : Orientation.Vertical;
            }

            // Apply the offset in the direction of the scroll.
            var scrollViewer = AssociatedScrollViewer;

            if (!_usesVirtualStackPanel.HasValue)
            {
                _usesVirtualStackPanel = false;
                if (InnerPresenter != null)
                {
                    var virtStackPanel = InnerPresenter.GetChild<VirtualizingStackPanel>();
                    _usesVirtualStackPanel = virtStackPanel != null;
                }
            }

            if (scrollViewer != null)
            {

                // Because the Deltas are coming in on the UI thread, and the action below is happening on the UI thread as well, performance issues
                // can show up.  By calling Dispatcher.BeginInvoke, the operation is deferred and the UI behaves more responsively.
                scrollViewer.Dispatcher.BeginInvoke(() =>
                {
                    // Calculate the target offset if the values are using pixels or items in the list (used by virt stackpanel)
                    Double targetOffset;
                    if (_scrollDirection == Orientation.Vertical)
                    {
                        if (_usesVirtualStackPanel == true)
                        {
                            var offsetDelta = e.Delta.TranslationY / scrollViewer.ActualHeight * scrollViewer.ViewportHeight;
                            targetOffset = Math.Min(scrollViewer.ScrollableHeight, Math.Max(0.0, scrollViewer.VerticalOffset - offsetDelta));
                        }
                        else
                        {
                            targetOffset = scrollViewer.VerticalOffset - e.Delta.TranslationY;
                        }
                        scrollViewer.ScrollToVerticalOffset(targetOffset);
                    }
                    else
                    {
                        if (_usesVirtualStackPanel == true)
                        {
                            var offsetDelta = e.Delta.TranslationY / scrollViewer.ActualWidth * scrollViewer.ViewportWidth;
                            targetOffset = Math.Min(scrollViewer.ScrollableHeight, Math.Max(0.0, scrollViewer.HorizontalOffset - offsetDelta));
                        }
                        else
                        {
                            targetOffset = scrollViewer.HorizontalOffset - e.Delta.TranslationX;
                        }
                        scrollViewer.ScrollToHorizontalOffset(targetOffset);
                    }
                    // It is necessary to call Update Layout, or the ScrollToXXXOffset method may not respond to all incoming Deltas.
                    scrollViewer.UpdateLayout();
                });
            }
        }

        /// <summary>
        /// Called when a manipulation completed on the <see cref="Behavior{T}.AssociatedObject"/>.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.Input.Manipulations.Manipulation2DCompletedEventArgs"/> instance containing the event data.</param>
        private void OnManipulationCompleted(Object sender, Manipulation2DCompletedEventArgs e)
        {
            _scrollDirection = null;

            if (InnerPresenter != null)
            {
                InnerPresenter.IsHitTestVisible = _originalHitTestVisibility;
            }
        }
        #endregion

        #region Abstract Methods & Properties

        /// <summary>
        /// Gets the associated scroll viewer.
        /// </summary>
        /// <value>The associated scroll viewer.</value>
        protected abstract ScrollViewer AssociatedScrollViewer { get; }

        #endregion
    }

    internal static class DependencyObjectExtensions
    {
        #region Helpers
        /// <summary>
        /// Gets the first child of the given type by walking the Visual Tree of the provided control
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="obj">The obj.</param>
        /// <returns></returns>
        public static T GetChild<T>(this DependencyObject obj) where T : DependencyObject
        {
            if (obj == null) throw new ArgumentNullException("obj");

            DependencyObject child = null; 
            for (Int32 i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
            {
                child = VisualTreeHelper.GetChild(obj, i);
                if (child != null && child.GetType() == typeof(T))
                {
                    break;
                }
                else if (child != null)
                {
                    child = GetChild<T>(child);
                    if (child != null && child.GetType() == typeof(T))
                    {
                        break;
                    }
                }
            }
            return child as T;
        }
        #endregion
    }
}